
//Testing author name correction
//Time zones: http://www.iana.org/time-zones

package calculationsandtests;

public class SundialCalculations {
	
	/*
	 * getHourLineAngles(double latitude, double longitude, int date)
	 *  
	 * Main calculation. Computes the angles of the lines to draw on the sundial
	 * Compensates for latitude, longitude deviation from the meridian and the EOT time of year variation
	 * (Earth's imperfect orbit etc.) 
	 * 
	 * If you are west of the meridian, the sun will show you a time earlier than what happens at the meridian (add time to correct)
	 * If you are in winter, the sun will show you a time earlier than official watch time at the meridian (add time to correct)
	 * West of the meridian in winter, both correction factors are negative. 
	 * 
	 * North and East are positive
	 * 
	 * @param
	 *   double latitude:  A double within -180 to +180
	 *   double longitude: A double within -180 to +180
	 *   int date:         An int of the format YYYYMMDD
	 *   
	 * @returns
	 *   double[]: an array representing the angles the lines needed to mark the hours on the sundial
	 *             the lines span approximately -90 to +90 (+/- a bit more due to EOT and Meridan corrections)
	 * 
	 */
	public static double[] getHourLineAngles(double latitude, double longitude, int date) {
		double[] angleArray = new double[13];
		double minutesMeridianDelta = getMeridianDelta(longitude) / 15 * 60; //minutes
		double EOTDelta = EOTCorrection(date); //minutes
		double netDelta = (minutesMeridianDelta + EOTDelta) / 60; //converted to hours
		//System.out.println(meridianDelta + " " + EOTDelta + " " + netDelta);
		//Lines are symmetrical before correction factors
		//After correction factors in winter, west of meridian (most common situation):
		//  Lines before noon are further from north
		//  Lines after noon are closer to north
		//  Noon is CCW of north
		double hoursFromNoon = -6;
		hoursFromNoon += netDelta; //Make bigger AM angles if modifier is negative (want to show a time later than what the sun shows)
		for(int i = 0; i <= 12; i++, hoursFromNoon++) {
			//System.out.println(hoursFromNoon);
			angleArray[i] = atanDegrees(tanDegrees(hoursFromNoon * 15)* sinDegrees(latitude));
		}
		//correction when angle should be obtuse (atan cannot spit out angle larger than 90)
		//Check and correct first entry
		if(angleArray[0] / angleArray[1] < 0) { //if signs dont match then we've gone past 90 degrees
			if(angleArray[1] < 0)
				angleArray[0] = -1 * (180 - angleArray[0]); //negative correction
			else
				angleArray[0] = 180 + angleArray[0]; //positive correction
		}
		//Check and correct last entry
		if(angleArray[12] / angleArray[11] < 0) { //if signs dont match then we've gone past 90 degrees
			if(angleArray[11] < 0)
				angleArray[12] = -1 * (180 - angleArray[12]); //negative correction
			else
				angleArray[12] = 180 + angleArray[12]; //positive correction
		}
		return angleArray;
	}
	
	/*
	 * 	public static int[] getLineLabels(double latitude, double longitude, int date, int dstFlag)
	 * @param
	 *   double latitude:  A double within -180 to +180
	 *   double longitude: A double within -180 to +180
	 *   int date:         An int of the format YYYYMMDD
	 *   int dstFlag:      0: use the algorithm to estimate 1: hard no on dst, 2: hard yes on dst 
	 *   
	 * @returns
	 *   int[]: an array of ints used to label each of the 13 lines generated by the
	 *          getHourLineAngles() method
	 */
	public static int[] getLineLabels(double latitude, double longitude, int date, int dstFlag) {
		int[] lineLabels = new int[13];
		for(int i = 0; i <= 12; i++) { 
			lineLabels[i] = i + 6;  //go from 6 am to 6 pm
			if(isDayLightSavings(latitude, longitude, date, dstFlag))
				lineLabels[i]++;   //just increase labels by 1 hr to handle DST correction
			
			if(lineLabels[i] > 12) //Convert military time (18:00) to civilian time (6:00)
				lineLabels[i] -= 12;
		}
		return lineLabels;
	}
	
	/* getGnomonAngle(double latitude)
	 * 
	 * may be wise to inform southern hemisphere users to point the gnomon
	 * at the south pole.
	 * 
	 * @returns 
	 *   double: the angle required for the gnomon 
	 */
	public static double getGnomonAngle(double latitude) {
		return Math.abs(latitude);
	}
	
	/* isNorthernHemisphere(double latitude) 
	 * @returns true if latitude >= 0, otherwise false
	 */
	public static boolean isNorthernHemisphere(double latitude) {
		return (latitude >= 0);
	}
	
	/*
	 * Gets the degrees of difference between current location and the meridian of the
	 * time zone you are currently located in.
	 * Bounds: +/- 7.5 degrees
	 * -7.5 degrees is west of the meridian
	 */
	protected static double getMeridianDelta(double longitude) {
		double meridianDelta = 0;
		int nearestHour = 0;
		nearestHour = (int)Math.round((longitude/15) * 1) / 1; //Calculate hours from GMT
		meridianDelta = longitude - nearestHour * 15; //Removes the hour difference to just leave the minute difference
		//System.out.println(nearestHour + " " + meridianDelta);
		return meridianDelta;
	}
	
	/*
	 * Returns the number of minutes to correct the sundial due to 
	 * orbital aberrations. 
	 * Formula from http://www.susdesign.com/popups/sunangle/eot.php
	 * E = 9.87 * sin (2B) - 7.53 * cos (B) - 1.5 * sin (B)
	 * B = 360 * (N - 81) / 365
	 * N = day number, January 1 = day 1	 
	 */
	protected static double EOTCorrection(int date) {
		double minutesDelta = 0;
		int dayNumber = getDayNumber(date);
		double B = 360 * (dayNumber - 81) / 365;
		double E = 9.87 * sinDegrees(2*B) - 7.53 * cosDegrees(B) - 1.5 * sinDegrees(B);
		minutesDelta = E;
		return minutesDelta;
	}
	
	/*
	 * getDayNumber(int date)
	 * 
	 * Used by the EOTCorrection() method
	 * 
	 * Get the number of the day in the year (1-365 or 366)
	 * Seems likely this code will be used in future projects so making a 
	 * separate method for it.
	 * Check vs. http://amsu.cira.colostate.edu/julian.html
	 */
	protected static int getDayNumber(int date) {
		int dayNumber = 0;
		int year = 0;
		int month = 0;
		int day = 0;
		boolean isLeapYear = false;
		int[] monthLengthStandard = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
		
		//break date into pieces
		day = date % 100;
		month = (int) (date % 10000) / 100;
		year = (int) date / 10000;

		//check for leapyear
		if((year % 400) == 0)
		   isLeapYear = true;
		else if ((year % 100) == 0)
		   isLeapYear = false;
		else if((year % 4) == 0)
		   isLeapYear = true;
		else
		   isLeapYear = false;
		
		//calculate day of the year
		dayNumber = day;
		for(int i = 0; i < month - 1; i++) {
			dayNumber += monthLengthStandard[i];
		}
		if(isLeapYear && month > 2) //add leap year day 
			dayNumber++;
		return dayNumber;
	}
	
	/*
	 * Gives cosine values. Takes angles in degrees.
	 */
	protected static double cosDegrees(double degrees) {
		return Math.cos(degrees/180*Math.PI);
	}
	
	/*
	 * Gives sine values. Takes angles in degrees.
	 */
	protected static double sinDegrees(double degrees) {
		return Math.sin(degrees/180*Math.PI);
	}
	
	/*
	 * Gives tangent values. Takes angles in degrees.
	 */
	protected static double tanDegrees(double degrees) {
		return Math.tan(degrees/180*Math.PI);
	}
	
	/*
	 * Calculates the angle in degrees, given a tangent value
	 */
	protected static double atanDegrees(double tan) {
		return Math.atan(tan)/Math.PI*180;
	}
	
	/*
	 * isDayLightSavings(double latitude, double longitude, int date)
	 * 
	 * Returns a boolean indicating if daylight savings correction is likely
	 * to be in effect for a given location and date.
	 * -This is not perfect. You need a 100 lawyers to be exactly correct
	 * -Changes happen at midnight, not 2am
	 * -Assuming US start and end dates
	 * 
	 * @param
	 *   double latitude:  A double within -180 to +180
	 *   double longitude: A double within -180 to +180
	 *   int date:         An int of the format YYYYMMDD
	 *   int dstFlag:      0: use the algorithm to estimate 1: hard no on dst, 2: hard yes on dst 
	 *   
	 * @returns
	 *   boolean: true if DST corrections should be applied
	 */
	protected static boolean isDayLightSavings(double latitude, double longitude, int date, int dstFlag) {
		boolean DSTBoolean = false;
		//user says no
		if(dstFlag == 1) {
			DSTBoolean = false;
		}
		//user says yes
		else if (dstFlag == 2) {
			DSTBoolean = true;
		}
		//user does not know
		else {
			try {
				//check if its summer and we're in a location that does DST (S-hemisphere not implemented yet)
				//Also, assuming USA DST dates for global usage
				if(hasDSTLocation(latitude, longitude) && isUSASummer(date)) {
					DSTBoolean = true;
				}
			}
			catch (Exception e) {
				System.out.println(e);
			}
		}
		return DSTBoolean;
	}
	
	/*
	 * isUSASummer(int date)
	 * 
	 * used by isDayLightSavings()
	 * Test if it is officially DST summer in the US.
	 * Time of day is ignored (simplification)
	 * dates before 2007 and after 2025 are not accepted and throw an exception
	 * Table from http://en.wikipedia.org/wiki/Daylight_saving_time_in_the_United_States
	 * 
	 */
	protected static boolean isUSASummer(int date) throws Exception {
		int[] USADSTSequence = {20070101, 20070311, 20071104, 20080309, 20081102, 20090308, 20091101, 20100314, 20101107, 20110313, 20111106, 20120311, 20121104, 20130310, 20131103, 20140309, 20141102, 20150308, 20151101, 20160313, 20161106, 20170312, 20171105, 20180311, 20181104, 20190310, 20191103, 20200308, 20201101, 20210314, 20211107, 20220313, 20221106, 20230312, 20231105, 20240310, 20241103, 20250309, 20251102, 20251231};
		boolean isSummer = true;
		if(date < 20070101 || date > 20251231) //error if date value is outside our table
			throw new Exception("DST module only works from 2007 to 2025.");
		//Iterate through DST sequence, toggling isSummer each value. When the wheel stops, isSummer will be correct.
		for (int i = 0; i < USADSTSequence.length && date >= USADSTSequence[i]; i++) {
			isSummer = !isSummer;
			//System.out.println(i % 2 + " " + isSummer + " " + date + " " + USADSTSequence[i]);
		}
		//System.out.println(isSummer);
		return isSummer;
	}
	
	/*
	 * used by isDayLightSavings()
	 * 
	 * Checks if area matches list of non-DST territories
	 * 
	 * Current Exclusions from DST:
	 *   Hawaii
	 *   
	 *   Does not handle crossing of international date line. 
	 *   Added zones that cross the IDL must be split into two zones bordered by +/- 180 longitude
	 */
	protected static boolean hasDSTLocation(double latitude, double longitude) {
		boolean isDSTLocation = true; //assume only exceptions don't do DST. (True in US and Europe and neither prof. or TA are from Asia)
		double hawaiiNLat = 30;
		double hawaiiSLat = 15;
		double hawaiiELong = -150;
		double hawaiiWLong = -170;

		//check if in hawaii
		if(withinGlobeQuandrant(latitude, longitude, hawaiiNLat, hawaiiSLat, hawaiiELong, hawaiiWLong))
			isDSTLocation = false;
		
		//if(!isDSTLocation)
		//	System.out.println("Your location does not observe daylight savings...ever.");
		return isDSTLocation;
	}
	
	/*
	 * withinGlobeQuandrant(double latitude, double longitude, double nLat, double sLat, double eLong, double wLong)
	 * 
	 * Used by hasDSTLocation() tests if a coordinate pair fits within a rectangle.
	 * Method created in case a more complicated boundary system is required in the future.
	 * 
	 * @params:
	 *   double latitude:  Latitude of test coordinate
	 *   double longitude: Longitude of test coordinate
	 *   double nLat:      Northern boundary of zone
	 *   double sLat:      Southern boundary of zone
	 *   double eLong:     Eastern boundary of zone
	 *   double wLong:     Western boundary of zone
	 * 
	 * Does not handle crossing of international date line. Zones crossing must be split into two zones bordered by +/- 180 longitude
	 * 
	 * @returns
	 *   boolean indicating if lat and long fall within the bound rectangle.
	 */
	protected static boolean withinGlobeQuandrant(double latitude, double longitude, double nLat, double sLat, double eLong, double wLong) {
		return (latitude <= nLat && latitude >= sLat && longitude <= eLong && longitude >= wLong);
	}
}

